%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
;-------------------------------构建GDT及其内部的描述符，此处占据512字节,即0x200
GDT_BASE:           dd 0x00000000           ;第0个段描述 符没用，所以全部定义为0
                    dd 0x00000000
CODE_DESC:          dd 0x0000FFFF           ;代码段描述符低32位
                    dd DESC_CODE_HIGH4
DATA_STACK_DESC:    dd 0x0000FFFF           ;数据段描述符低32位
                    dd DESC_DATA_HIGH4
VIDEO_DESC:         dd 0x80000007       ;limit=(0xbffff-0xb8000)/4k=0x7
                    dd DESC_VIDEO_HIGH4 ;此时dpl为0
GDT_SIZE    equ   $ - GDT_BASE
GDT_LIMIT   equ   GDT_SIZE - 1
times 60 dq 0                ;预留60个描述符的空位
;---------------构建选择子，宏定义不占据具体内存
SELECTOR_CODE  equ (0x0001<<3) + TI_GDT + RPL0   ;相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0    意为第一个段描述符，index为1
SELECTOR_DATA  equ (0x0002<<3) + TI_GDT + RPL0   ;同上
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0   ;同上

;total_mem_bytes用于保存内存容量，以字节为单位，占4字节
total_mem_bytes dd 0

;以下是gdt指针，前2字节是gdt界限，后4字节时gdt起始地址
gdt_ptr   dw   GDT_LIMIT
          dd   GDT_BASE
;人工对齐：total_mem_bytes4+gdt_ptr6+ards_buf244+ards_nr2,共256字节,0x100
ards_buf  times 244 db 0        ;ARDS缓存
ards_nr   dw 0                  ;记录ARDS结构体数量

;又由于上边的定义，导致此处在内存中的位置为LOADER_BASE_ADDR+0x300
loader_start:
;----------------------int 15h eax = 0000E820h,edx = 534D4150h('SMAP')获取内存布局
    xor ebx,ebx             ;ebx值清0
    mov edx,0x534d4150      ;edx只赋值一次，循环体中不会改变
    mov di,ards_buf         ;ards结构缓冲区
  .e820_mem_get_loop:
    mov eax,0x0000e820      ;执行 int 0x15 后， eax 值变为 0x534d4150,所以每次执行int前都要更新为子功能号
    mov ecx,20              ;ARDS地址范围描述符结构大小是20字节
    int 0x15
    jc .e820_failed_so_try_e801     ;若CF位为0表示调用未出错，CF为1表示调用出错，出错则跳转 意为若cf位为1则有错误发生，尝试0xe801子功能
    add di,cx               ;使di增加20字节指向缓冲区置嗯新的ARDS结构位置
    inc word [ards_nr]      ;记录ARDS数量
    cmp ebx,0               ;若ebx为0且cf不为1,这说明该ARDS是最后一个ARDS
    jnz .e820_mem_get_loop
;在所有ards结构中，找出（base_add_low+lenght_low）最大值，即内存的容量
    mov cx,[ards_nr]        ;遍历每一个ARDS结构体，循环次数是 ARDS 的数量
    mov ebx,ards_buf
    xor edx,edx             ;edx为最大的内存容量，在此先清0
  .find_max_mem_area:
    mov eax,[ebx]           ;base_add_low
    add eax,[ebx+8]         ;length_low
    add ebx,20              ;指向缓冲区下一个ARDS结构
    cmp edx,eax
    ;冒泡排序，找出最大，edx寄存器始终是最大的内存容量
    jge .next_ards
    mov edx,eax             ;edx为最大内存大小--如果当前eax大于edx(最大的内存)，则进行赋值
  .next_ards:
    loop .find_max_mem_area ;根据cx执行loop次数
    jmp .mem_get_ok

;-----------------------int 15h ax = E801h 获取内存大小，最大支持4G
;返回后， ax cx 值一样，以 KB 为单位， bx dx 值一样，以 64KB 为单位
;在 ax 和 cx 寄存器中为低 16MB ，在 bx 和 dx 寄存器中为 16MB 到 4GB
  .e820_failed_so_try_e801:
    mov ax,0xe801
    int 0x15
    jc .e801_failed_so_try88   ;若当前e801方法失败，就尝试0x88方法
;1.先算出低 15MB 的内存
; ax和ex 中是以 KB 为单位的内存数量，将其转换为以 byte 为单位
    mov cx,0x400               ;cx和ax值一样，cx用作乘数
    mul cx
    shl edx,16
    and eax,0x0000FFFF
    or  edx,eax
    add edx,0x100000            ;ax只是15M,故要加1MB
    mov esi,edx                 ;先把低15MB的内存容量存入esi寄存器备份
;2.再将 16MB 以上的内存转换为 byte 为单位
;寄存器 bx和dx 中是以 64KB 为单位的内存数量
    xor eax,eax
    mov ax,bx
    mov ecx,0x10000             ;0x10000十进制为64KB
    mul ecx                     ;32位乘法，默认的被乘数是eax,积为64位  高32位存入edx,低32位存入eax
    add esi,eax
;由于此方法只能测出 4GB 以内的内存，故 32 eax 足够了; edx 肯定为0，只加 eax 便可
    mov edx,esi
    jmp .mem_get_ok
;-----------------int 15h ah = 0x88 获取内存大小，只能获取 64MB 之内
  .e801_failed_so_try88:
;int 15 后， ax 存入的是以 KB 为单位的内存容量
    mov ah,0x88
    int 0x15
    jc .error_hlt
    and eax,0x0000FFFF
    ;16 位乘法，被乘数是 ax ，积为 32 位。积的高 16 位在 dx中，积的低16位在ax中
    mov cx,0x400
;Ox400 等于 1024 ，将 ax 中的内存容量换为以 byte 为单位
    mul cx
    shl edx,16                  ;把 dx 移到高16位
    or  edx,eax                  ;把积的低16位组合到edx,为32位的积
    and edx,0x100000            ;Ox88 子功能只会返回 1MB 以上的内存  ,故实际内存大小要加上1MB
    jmp .mem_get_ok

.error_hlt:
    mov byte [gs:0], 'f'
    mov byte [gs:2], 'a'
    mov byte [gs:4], 'i'
    mov byte [gs:6], 'l'
    mov byte [gs:8], 'e'
    mov byte [gs:10], 'd'
    jmp $

.mem_get_ok:
    mov [total_mem_bytes], edx  ;将内存换为byte单位后存入total_mem_bytes处
;---------------------准备进入保护模式------------------------
;1.打开A20
;2.加载gdt
;3.将cr0的pe位置1
;------------打开A20
    in al,0x92
    or al,0000_0010B
    out 0x92,al
;------------加载GDT
    lgdt [gdt_ptr]
;------------cr0第0位置1
    mov eax,cr0
    or eax,0x00000001
    mov cr0,eax
    jmp dword SELECTOR_CODE:p_mode_start   ;刷新流水线

[bits 32]
p_mode_start:
    mov ax,SELECTOR_DATA
    mov ds,ax
    mov es,ax
    mov ss,ax
    mov esp,LOADER_STACK_TOP
    mov ax, SELECTOR_VIDEO
    mov gs, ax
;--------------加载kernel----------------------
    mov eax,KERNEL_START_SECTOR     ;kernel.bin 所在的扇区号
    mov ebx,KERNEL_BIN_BASE_ADDR    ;从磁盘读出后，写入到 ebx 指定的地址
    mov ecx,200                     ;读入的扇区数

    call rd_disk_m_32

    call setup_page
    ; 保存gdt表
    sgdt [gdt_ptr]
    ; 重新设置gdt描述符， 使虚拟地址指向内核的第一个页表
    mov ebx,[gdt_ptr + 2]
    or  dword [ebx + 0x18 +4], 0xc0000000
    add dword [gdt_ptr + 2], 0xc0000000
    add esp, 0xc0000000

    ; 页目录基地址寄存器给到cr3
    mov eax, PAGE_DIR_TABLE_POS
    mov cr3, eax

    ; 打开分页
    mov eax, cr0
    or eax, 0x80000000
    mov cr0, eax
    ;开启分页后，用gdt新的地址重新加载
    lgdt [gdt_ptr]

    ; 初始化kernel
    jmp SELECTOR_CODE:enter_kernel
;-----------------进入内核（C语言）-------------------
enter_kernel:
    call kernel_init
    mov esp, 0xc009f000

    jmp KERNEL_ENTRY_POINT



;-----------创建页目录以及页表---------
setup_page:
    mov ecx, 4096
    mov esi, 0
.clear_page_dir:    ;利用循环将页目录表占据4KB空间清零之
    mov byte [PAGE_DIR_TABLE_POS + esi],0
    inc esi
    loop .clear_page_dir

.create_pde:  ;创建页目录表项(PDE)
    mov eax, PAGE_DIR_TABLE_POS
    add eax, 0x1000    ;1024个页目录表项，每个4B,所以4096B之后为页表项
    mov ebx, eax    ;此处为 ebx 赋值，是为 .create_pte 做准备， ebx 为基址

    or  eax, PG_US_U|PG_RW_W|PG_P   ;页目录项的属性 RW 位为 1, us ，表示用户属性，所有特权级别都可以访问
    mov [PAGE_DIR_TABLE_POS + 0x0],eax  ;在页目录表中的第1目录项写入第一个页表的位置（ 0x101000 ）及属性（ 7)
    mov [PAGE_DIR_TABLE_POS + 0xc00],eax ;一个页表项占用4字节,0xc00表示第768个页表占用的目录项，0xc00以上的目录项用于内核空间;划分方法  0～3G位用户空间，3～4G为内核空间;1024个页表项代表4G,那么768个页表以上则为3～4G空间
    sub eax,0x1000                       ;最后一个表项指向自己，用于访问页目录本身
    mov [PAGE_DIR_TABLE_POS+4092],eax    ;1023*4=4092
;创建页表项(PTE)
    mov ecx,256    ;1M 低端内存／每页大小 4k = 256 次循环   虚拟地址的0~0x3fffff映射到物理地址0~0x3fffff
    mov esi,0
    mov edx,PG_US_U|PG_RW_W|PG_P
.create_pte:
    mov [ebx+esi*4],edx         ;0x100000 ==>0x00000007     0x100004==>0x00001007   0x100008==>0x00002007
    add edx,4096
    inc esi
    loop .create_pte
;创建内核其他页表的 PDE
    mov eax,PAGE_DIR_TABLE_POS
    add eax,0x2000              ;此时 eax 为第二个页表的位置
    or  eax,PG_US_U|PG_RW_W|PG_P ;页目录项的属性 US、 RW 位和P位都为1
    mov ebx,PAGE_DIR_TABLE_POS
    mov ecx,254                 ;范围为第 769-1022 的所有目录项数
    mov esi,769
.create_kernel_pde:
    mov [ebx+esi*4],eax
    inc esi
    add eax,0x1000
    loop .create_kernel_pde
    ret

; 保护模式的硬盘读取函数
rd_disk_m_32:

    mov esi, eax
    mov di, cx

    mov dx, 0x1f2
    mov al, cl
    out dx, al

    mov eax, esi

    mov dx, 0x1f3
    out dx, al

    mov cl, 8
    shr eax, cl
    mov dx, 0x1f4
    out dx, al

    shr eax, cl
    mov dx, 0x1f5
    out dx, al

    shr eax, cl
    and al, 0x0f
    or al, 0xe0
    mov dx, 0x1f6
    out dx, al

    mov dx, 0x1f7
    mov al, 0x20
    out dx, al

.not_ready:
    nop
    in al, dx
    and al, 0x88
    cmp al, 0x08
    jnz .not_ready

    mov ax, di
    mov dx, 256
    mul dx
    mov cx, ax
    mov dx, 0x1f0

.go_on_read:
    in ax, dx
    mov [ebx], ax           ;注意这里已经是32位，只取bx(16位)会出现错误
    add ebx, 2
    loop .go_on_read

    ret
;-----------------加载内核-------------------
kernel_init:
    xor eax, eax
    xor ebx, ebx        ;ebx 记录程序头表地址
    xor ecx, ecx        ;cx 记录程序头表中的 program header 数量
    xor edx, edx        ;dx 记录 program header 尺寸，即 e_phentsize

    mov dx,  [KERNEL_BIN_BASE_ADDR + 42]         ;偏移文件42字节处的属性是e_phentsize,表示每个program header entry size 大小
    mov ebx, [KERNEL_BIN_BASE_ADDR + 28]         ;偏移文件开始部分28字节的地方是 e_phoff,代表程序头开始偏移量

    add ebx, KERNEL_BIN_BASE_ADDR
    mov cx,  [KERNEL_BIN_BASE_ADDR + 44]         ;偏移文件开始部分 44 字节的地方是 e_phnum ，表示有几个 program header

.each_segment:
    cmp byte [ebx + 0], PT_NULL                 ;若 p_type 等于 PT NULL ，说明此 program header 未使用
    je .PTNULL

    ; 准备mem_cpy参数
    push dword [ebx + 16]                       ; program header 中偏移 16 字节的地方是 filesz  ;压入函数 memcpy 的第三个参数 size
    mov eax, [ebx + 4]                          ;距程序头偏移量为 字节的位置是 offset
    add eax, KERNEL_BIN_BASE_ADDR
    push eax                                    ;压入函数 memcpy 的第二个参数源地址
    push dword [ebx + 8]                        ;压入函数 memcpy 的第一个参数，目的地址

    call mem_cpy
    add esp, 12

.PTNULL:
    add ebx, edx        ;跳过当前程序头表项
    loop .each_segment
    ret

mem_cpy:
    cld
    push ebp
    mov ebp, esp
    push ecx

    mov edi, [ebp + 8]          ;dst
    mov esi, [ebp + 12]         ;src
    mov ecx, [ebp + 16]         ;size
    rep movsb

    pop ecx
    pop ebp
    ret

